﻿using System.Windows.Controls;
using DeepEarth.Map.Core.Utilities;
using System.Windows;
using System.Windows.Media;
using DeepEarth.Map.Core;
using DeepEarth.Core;
using DeepEarth.Map.Core.Clustering;
using System;
using System.Collections.Generic;
using System.Linq;
using DeepEarth.Core.Data;

namespace DeepEarth.Map.Core
{
    public class VectorCelLayer : UIElementLayer
    {        
        public const double DefaultCelWidth = 32767;
        public const double BingMapsMaxLatitiude = 85.051128;
        public const double DefaultClippingBounds = 16848;

        public LocationRectangle BoundingBox { get; set; }

        public Point Origin { get; set; }

        public double LogicalWidth { get; set; }

        CompositeTransform compositeTransform;
        protected CompositeTransform CompositeTransform
        {
            get
            {
                return compositeTransform;
            }
            set
            {
                compositeTransform = value;
                this.RenderTransform = value;
            }
        }
        Point zeroOffset = new Point(0, 0);

        public VectorCelLayer() : this(DefaultCelWidth)
        {
        }

        protected VectorCelLayer(double width)
        {
            CurrentScale = double.NaN;
            this.Width = width;
            this.Height = width;
            CompositeTransform = new CompositeTransform();

            this.clippingBounds = width;
        }

        public void Add(UIElement element)
        {
            ISelfPositioning sp = element as ISelfPositioning;
            sp.LocationToPoint = this.LocationToCelPixel;

            sp.Refresh(zeroOffset, clippingBounds);

            this.Children.Add(element);
        }

        public Point LocationToCelPixel(double latitude, double longitude)
        {
            Location location = new Location
            {
                Longitude = longitude,
                Latitude = latitude
            };

            return LocationToCelPixel(location);
        }

        public virtual Point LocationToCelPixel(Location location)
        {
            Point logical = CoordinateTransformation.GeographicToLogical(location);

            // need to adjust the global logical coordinate to this cel's logical coordinate.
            // e.g. If this cel is 0.5 global units wide with an origin of 0.5,0.5 (global), then 0.8,0.8 in global coordinates is 0.6,0.6 in
            // cell coordinates.

            double tempX = logical.X - Origin.X;
            double tempY = logical.Y - Origin.Y;

            double adj = 1.0 / LogicalWidth;

            Point relative = new Point(tempX * adj, tempY * adj);

            return new Point
            {
                X = this.Width * relative.X,
                Y = this.Height * relative.Y
            };
        }

        public void Synchronize()
        {
            SynchronizeOnFrame();
        }

        protected override void SynchronizeOnFrame()
        {
            if (CheckVisibility())
            {
                this.Visibility = Visibility.Visible;
                CalculateTransforms();
                ExecuteTransforms();

                ApplyScaling();
            }
            else
            {
                this.Visibility = Visibility.Collapsed;
            }
        }

        protected override void SynchronizeOnEnd()
        {
            ApplyScaling();
        }

        double lastScale = -1;
        double lastEndScale = -1;
        protected double CurrentScale { get; set; }

        protected double TargetXTranslate { get; set; }
        protected double TargetYTranslate { get; set; }

        double lastAppliedScale;
        double lastAppliedXTranslate;
        double lastAppliedYTranslate;

        Point zeroPoint = new Point(0, 0);

        double clippingBounds;

        protected virtual bool CheckVisibility()
        {
            return MapInstance.BoundingRectangle.Collides(this.BoundingBox);
        }

        protected virtual void CalculateTransforms()
        {
            // It is possible for the user to trigger a synchronize before we're fully loaded.
            if (MapInstance == null)
            {
                return;
            }

            // calculate required scale
            double viewportWidth = MapInstance.CurrentViewportLogicalWidth;
            double currentWorldWidthAtView = MapInstance.AsFrameworkElement.ActualWidth / viewportWidth;

            double logicalFraction = 1d / LogicalWidth;

            double currentCellWidthAtView = currentWorldWidthAtView / logicalFraction;

            CurrentScale = currentCellWidthAtView / this.Width;

            var point = MapInstance.LocationToViewportPoint(this.BoundingBox.NorthWest);

            this.TargetXTranslate = point.X;
            this.TargetYTranslate = point.Y;

            if (CurrentScale > 1)
            {
                clippingBounds = DefaultClippingBounds;
            }
            else
            {
                clippingBounds = Width;
            }
        }

        protected virtual void ExecuteTransforms()
        {
            if (lastAppliedScale != CurrentScale || lastAppliedXTranslate != TargetXTranslate || lastAppliedYTranslate != TargetYTranslate)
            {
                lastAppliedScale = CurrentScale;
                lastAppliedXTranslate = TargetXTranslate;
                lastAppliedYTranslate = TargetYTranslate;

                this.CompositeTransform.TranslateX = (TargetXTranslate);
                this.CompositeTransform.TranslateY = (TargetYTranslate);

                if (lastScale != CurrentScale)
                {
                    this.CompositeTransform.ScaleX = CurrentScale;
                    this.CompositeTransform.ScaleY = CurrentScale;

                    lastScale = CurrentScale;
                }
            }
        }

        void ApplyScaling()
        {
            if (!double.IsNaN(CurrentScale) && lastEndScale != CurrentScale)
            {
                ApplyInverseScaling(CurrentScale);
                ApplyPerFeatureScaling(CurrentScale);

                lastEndScale = CurrentScale;
            }
        }

        protected virtual void ApplyInverseScaling(double scale)
        {
            foreach (var kvp in referenceElements)
            {
                MapItemAdorner adorner = kvp.Value;
                adorner.OverlayScaleTransform.ScaleX = 1.0 / scale;
                adorner.OverlayScaleTransform.ScaleY = 1.0 / scale;
            }
        }

        protected virtual void ApplyPerFeatureScaling(double scale)
        {
            double viewportWidth = MapInstance.CurrentViewportLogicalWidth;
            double zoomFactor = Math.Log(1 / viewportWidth, 2);
            double dynamicScale = Math.Pow(0.02 * (zoomFactor + 1), 2) + 0.01;

            // the entire panel is shrunk
            // Apply an inverse scaling on the elements so they are visible.
            foreach (var kvp in referenceElements)
            {
                MapItemAdorner adorner = kvp.Value;

                if (adorner.Content is IScalable)
                {
                    IScalable scalable = adorner.Content as IScalable;
                    scalable.ApplyScale(dynamicScale, 1.0 / scale);
                }
            }
        }

        int lastSynchronizationChildCount = 0;
        Dictionary<ISelfPositioning, short> added = new Dictionary<ISelfPositioning, short>();

        protected override void ChildAdded()
        {
            if (base.Children.Count != lastSynchronizationChildCount)
            {
                if (base.Children.Count < lastSynchronizationChildCount)
                {
                    added.Clear();
                }

                lastSynchronizationChildCount = base.Children.Count;

                // This is untidy :(
                // Memory Risk
                Dispatcher.BeginInvoke(() =>
                {
                    double viewportWidth = MapInstance.CurrentViewportLogicalWidth;
                    double zoomFactor = Math.Log(1 / viewportWidth, 2);
                    double dynamicScale = Math.Pow(0.02 * (zoomFactor + 1), 2) + 0.01;

                    if (lastEndScale != CurrentScale)
                    {
                        CalculateTransforms();
                    }

                    var outOfPosition = BaseChildren.OfType<ISelfPositioning>().Where(c => !added.ContainsKey(c));

                    foreach (ISelfPositioning sp in outOfPosition)
                    {
                        added.Add(sp, 0);
                        sp.Refresh(new Point(0, 0), clippingBounds);

                        if (sp is IScalable)
                        {
                            IScalable sc = sp as IScalable;
                            sc.ApplyScale(dynamicScale, 1.0 / CurrentScale);
                        }
                    }
                }
                );
            }
        }

    }
}
